from pystock.portfolio import Portfolio, Stock
from pystock.models import Model
from pystock.FFF import FamaFrenchFactors
import warnings
import plotly.io as pio
pio.renderers.default = "notebook"
warnings.filterwarnings("ignore")
FamaFrenchFactors class¶This class is used to download and load Fama French factors. Start by creating an instance of the class.
fff = FamaFrenchFactors()
To download the factors, use the download function. It takes the following parameters:
frequency : str, optional
The frequency of the data. The default is "D".
factors : int, optional
The number of factors. The default is 3. Possible values are 3 and 5
directory : str, optional
The directory to save the file. The default is ".".
overwrite : bool, optional
Whether to overwrite the file if it already exists. The default is False.
factors has two possible values, 3 and 5.
file_path = fff.download(frequency="M", factors=5, directory=".", overwrite=True)
load function¶Once downloaded, the fff can be loaded using the load function. The function takes the following params:
directory : str, optional
The directory to save the file. The default is ".".
frequency : str, optional
The frequency of the data. The default is "M".
factors : int, optional
The number of factors. The default is 3. Possible values are 3 and 5
preprocess : bool, optional
Whether to preprocess the data. The default is True.
fff5 = fff.load(frequency="M", factors=5, directory=".", preprocess=True)
fff5
These factors will be used for fff3 and fff4 models later. For now, we'll have a look at some more things which you can do with the FamaFrenchFactors class.
You can change the frequency of the factors using the change_frequency function. It takes just one parameter:
frequency : str, optional
The frequency of the data. The default is "D".
fff5_quarterly = fff.change_frequency(frequency="Q")
fff5_quarterly
The function changes the frequency of data inplace, meaning that if you want to upsample the data (i.e. change frequency from month to day), you will get wrong results. The function uses
ffillto fill the missing values so changing frequency from month to day will result in the same value for all the days in the month.
In Fama-French model, we'll need the mean of the columns for calculating the expected return of stock. The class provides a function to do this:
means = fff.calculate_mean_values()
means
Note that there is an extra value named const. This is here because the Fama-French model has a constant term. Using mean in this form makes it easy to calculate the expected return of a stock.
Stock class¶Stock object¶Start by loading the Stock class from the pystock module:
apple = Stock("AAPL", "Data/AAPL.csv")
apple
Let's see what the Stock object has:
apple.__dict__
return_ is a dictionary which will contain the return of the stock, it can be a float (if you want mean return) or a pd.Series of floats (if you want to get the return of each day).
fff is a reference to the FamaFrenchFactors object. We will see later what it is. loaded is a boolean equal to True if the stock data has been loaded, False otherwise. Let's load the data. The load function takes a number of parameters:
start_date : str, optional
Start date of the data, by default None
end_date : str, optional
End date of the data, by default None
columns : list, optional
Columns to keep, by default None which means keep all columns
frequency : str, optional
Frequency of the data, by default "D"
rename_cols : list, optional
Columns to rename, by default None
The function returns a pd.DataFrame with the data. Let's see what the data looks like:
load_data function¶start_date = "2010-01-01"
end_date = "2022-12-20"
frequency = "D"
apple.load_data(start_date=start_date, end_date=end_date, frequency=frequency)
apple.__dict__.keys()
The Stock object now has some more attributes. data is a pd.DataFrame with the data. start_date and end_date are the start and end dates of the data. columns is a list of the columns of the data. frequency is the frequency of the data.
apple.data.head()
apple.loaded
As you can see, loaded is now equal to True.
Next, we'll calculate various returns using the object. For this, we have the freq_return function having the following parameters:
frequency : str, optional
Frequency of the data, by default "M"
mean : bool, optional
Whether to return the mean of the return, by default True
column : str, optional
Column to calculate the return, by default "Close"
daily_return_series = apple.freq_return(frequency="D", mean=False)
daily_return_avg = apple.freq_return(frequency="D", mean=True)
display(daily_return_series.head())
display(daily_return_avg)
monthly_return_series = apple.freq_return(frequency="M", mean=False)
monthly_return_avg = apple.freq_return(frequency="M", mean=True)
display(monthly_return_series.head())
display(monthly_return_avg)
These returns are saved in the return_ attribute of the object. Note that the key of the dictionary return_ is the frequency of the return. So, it will save the mean of the returns as that was what calculated last.
apple.return_
apple.frequency
apple.data.head()
The data was loaded with a frequency of day. Suppose you want to change it to some other frequency. This can be done by the change_frequency function. It takes just one parameter:
frequency : str
Frequency of the data
apple.change_frequency("M")
apple.frequency
apple.data.head()
The function changes the frequency of data inplace, meaning that if you want to upsample the data (i.e. change frequency from month to day), you will get wrong results. The function uses
ffillto fill the missing values so changing frequency from month to day will result in the same value for all the days in the month.
Stock object with FamaFrenchFactors¶The fff attribute of a Stock object is reference to a FamaFrenchFactors object. This object is used to get the Fama-French factors. See the corresponding section for more details. Here, we'll give a brief overview of how to use it.
download_data function¶This is a wrapper function for FamaFrenchFactors.download. It takes the same parameters as FamaFrenchFactors.download (Along with some other params like load) and returns the same thing. It is used to download the data from the Fama-French website. Again, see the corresponding section for more details.
The load parameter is used to load the data into the FamaFrenchFactors object. If load is True, then the data is loaded into the fff attribute of the Stock object. If load is False, then the data is not loaded only downloaded. This is useful if you want to download the data and then load it later.
fff_data = apple.download_fff(frequency="D", factors=5, directory="Data", load=True)
Since we have used load=True, the data is loaded into the fff attribute of the Stock object.
apple.fff.data
load_fff function¶This is a wrapper function for FamaFrenchFactors.load. It takes the same parameters as FamaFrenchFactors.load and returns the same thing. It is used to load the data from local if it exists.
apple.load_fff(frequency="D", factors=5, directory="Data")
The factors can be calculated using the calculate_fff function. It takes the following parameters:
column : str, optional
Column to calculate the fama french factors on, by default "Close"
verbose : int, optional
Verbosity, by default 1
The function will throw error if either the Stock or the FamaFrenchFactors object is not loaded.
params = apple.calculate_fff(column = "Close")
params
Portfolio class¶The class represents a portfolio which has a list of stocks and a benchmark. You can also provide a weight for each stock.
Portfolio¶To start, you have to at least provide the directory of the benchmark data as well as its name. You must also provide at least one stock. You can also provide a list of stock names and their directory. The weight can also be provided. If not provided (which defualts to "equal"), then the weight will be equal to 1/n where n is the number of stocks.
def __init__(self, benchmark_dir, benchmark_name, stocks_dir=None, stocks_name=None, weights=None):
benchmark_name = "S&P"
benchmark_dir = "Data/GSPC.csv"
portfolio = Portfolio(benchmark_dir=benchmark_dir, benchmark_name=benchmark_name)
portfolio
len(portfolio)
The representation of portfolio shows the name of benchmark and the stocks in the portfolio. The length of the portfolio is the number of stocks in the portfolio (Including the benchmark).
Portfolio¶portfolio.benchmark.loaded
Right now, portfolio has just one unloaded benchmark and no stocks. Let's load the benchmark and add a stock.
This can be done by using the load_benchmark function. It takes the following parameters:
start_date : str, optional
Start date of the data, by default None
end_date : str, optional
End date of the data, by default None
columns : list, optional
Columns to keep, by default None which means keep all columns
frequency : str, optional
Frequency of the data, by default "D"
rename_cols : list, optional
Columns to rename, by default None
start_date = "2012-01-01"
end_date = "2022-12-20"
frequency = "D"
portfolio.load_benchmark(start_date=start_date, end_date=end_date, frequency=frequency)
portfolio.benchmark.loaded
portfolio.benchmark.data.head()
Alternatively, you can use the
Stock.load_datafunction to load the benchmark data since benchmark is just aStockobject.
You can also change the benchmark by using the change_benchmark function. It takes the following parameters:
benchmark_dir : str
Directory of the benchmark
benchmark_name : str
Name of the benchmark
load : bool, optional
Load the data, by default True
use_prev : bool, optional
Use the values of start_date, end_date, columns, frequency, rename_cols from the previous benchmark, by default True
start_date : str, optional
Start date, by default None
end_date : str, optional
End date, by default None
columns : list, optional
Columns to keep, by default None
frequency : str, optional
Frequency of the data, by default "D"
rename_cols : list, optional
Columns to rename, by default None
dji_name = "Dow_Jones"
dji_dir = "Data/DJI.csv"
portfolio.change_benchmark(benchmark_dir=dji_dir, benchmark_name=dji_name, load=True, use_prev=False)
portfolio
portfolio.benchmark.loaded
The class provides a function add_stocks to add a stock. It takes the following parameters:
stock_dirs : list
List of stock directories
stock_names : list, optional
List of stock names, by default None
load_data : bool, optional
Whether to load the data, by default True
start_date : str, optional
Start date, by default None
end_date : str, optional
End date, by default None
columns : list, optional
Columns to keep, by default None
frequency : str, optional
Frequency of the data, by default "D"
rename_cols : list, optional
Columns to rename, by default None
overwrite : bool, optional
Whether to overwrite existing stocks, by default False
The quickest way to add a single or a number of stock is by passing the stock_dirs and stock_names parameter. Let's see this in action:
stock_names = ["AAPL"]
stock_dirs = ["Data/AAPL.csv"]
portfolio.add_stocks(stock_dirs = stock_dirs, stock_names = stock_names, load_data=False, frequency=frequency, start_date=start_date, end_date=end_date)
If we want to add a single stock, give the name and directory of stock inside a list. This is what we have done here.
portfolio
Stock Object¶Another way is to first create the Stock object and then add it using the same method.
google = Stock("GOOG", "Data/GOOG.csv")
portfolio.add_stocks(stocks=[google], load_data=False, frequency=frequency, start_date=start_date, end_date=end_date)
portfolio
Now, our portfolio has one benchmark and two stock.
portfolio.weights
You can see that the weights has been adjusted.
For this, just pass a list of stock directories and names. The weights will be adjusted accordingly. Or you can pass a list of Stock objects.
stock_names = ["TSLA", "MSFT"]
stock_dirs = ["Data/TSLA.csv", "Data/MSFT.csv"]
portfolio.add_stocks(stock_dirs = stock_dirs, stock_names = stock_names, load_data=False, frequency=frequency, start_date=start_date, end_date=end_date)
portfolio
Another thing to note is that two
Stocksare considered equal if they have the same name. You can not have two stocks with the same name in the portfolio. If you try to add a stock with the same name as an existing stock, then the existing stock will be overwritten or the command will be ignored depending on the value ofoverwriteparameter.
google = Stock("GOOG", "Data/GOOG.csv")
portfolio.add_stocks(stocks=[google], load_data=False, frequency=frequency, start_date=start_date, end_date=end_date, overwrite=False)
portfolio
google = Stock("GOOG", "Data/GOOG.csv")
portfolio.add_stocks(stocks=[google], load_data=False, frequency=frequency, start_date=start_date, end_date=end_date, overwrite=True)
portfolio
To remove a Stock from Portfolio, use the remove_stock function. It takes the following parameters:
names : list
A list names of the stock to remove
portfolio.remove_stocks(["GOOG"])
portfolio
To change the frequency of the portfolio, use the change_benchmark_frequency function. It takes the following parameters:
frequency : str
Frequency of the data
change_stocks : bool, optional
Whether to change the frequency of the stock data, by default True
portfolio.benchmark.frequency
However, you can change the frequency only if you have loaded the data. If you have not loaded the data, then the function will throw an error.
portfolio.change_benchmark_frequency("M")
Loading data will be covered in the next section. For now, as benchmark is already loaded, we will change the frequency of the benchmark.
portfolio.change_benchmark_frequency("M", change_stocks=False)
portfolio.benchmark.frequency
Although you can get away with changing the frequency of the benchmark only, it is recommended to change the frequency of the stock data as well.
Many times, when we try to run some function, you will get an exception telling that "'Stock' object has no attribute 'data'". This happens because the Stock is not loaded yet as you can check by using the loaded attribute of the Stock object.
for stock, name in portfolio:
print(name, stock.loaded)
We see that no stock data is loaded. Let's load the data.
ou can use the
Portfolioas an iterator. Some more details about these special methods will be covered later.
There are mainly three functions to load data. We already discussed the load_benchmark function. Other two are discussed below.
load_one_stock¶As the name suggests, this loads data of one stock specified by the name parameter. The function is built on Stock.load_data function. It takes the following parameters:
name : str
Name of the stock
start_date : str, optional
Start date, by default None
end_date : str, optional
End date, by default None
columns : list, optional
Columns to keep, by default None
frequency : str, optional
Frequency of the data, by default "D"
rename_cols : list, optional
Columns to rename, by default None
overwrite : bool, optional
Whether to overwrite existing data, by default False
apple_data = portfolio.load_one_stock("AAPL", frequency=frequency, start_date=start_date, end_date=end_date)
for stock, name in portfolio:
print(name, stock.loaded)
The data of APPL is now loaded. We get some more attributes by loading the data. See the Stock class for more details.
load_all¶As the name suggests, this loads data of all the stocks in the portfolio. It takes the following parameters:
start_date : str, optional
Start date, by default None
end_date : str, optional
End date, by default None
columns : list, optional
Columns to keep, by default None
frequency : str, optional
Frequency of the data, by default "D"
rename_cols : list, optional
Columns to rename, by default None
overwrite : bool, optional
Whether to overwrite existing data, by default False
Previously, we just loaded the apple data, now we'll load all the data.
portfolio.load_all(frequency=frequency, start_date=start_date, end_date=end_date)
for stock, name in portfolio:
print(name, stock.loaded)
Let's see the data of these stocks.
portfolio["AAPL"].data.head()
portfolio["TSLA"].data.head()
The Portfolio object has implemented the __repr__ method which lets it represent the object in understandable manner.
portfolio
You can see that the represenation of Portfolio has the name of the benchmark and the list of the stocks. This lets us have a "peek" at the portfolio!
You can "print" the Portfolio and it will give a peek of the portfolio:
print(portfolio)
in Keyword¶The Portfolio class implements the __contains__ special method. This makes it easy to use the in keyword to check if a Stock is in the Portfolio. Use the stock name or the Stock object.
"AAPL" in portfolio, "TCS" in portfolio
portfolio.stocks[0] in portfolio
portfolio.benchmark in portfolio
You can use the name of the stock to get the Stock from the Portfolio object:
portfolio["AAPL"]
portfolio["Dow_Jones"]
You can iterate over the Portfolio.
for stock, name in portfolio:
print(stock.name, name)
The Portfolio iterator yields the Stock and name of the stock. Note that the first entry is that of the benchmark.
You can use the list constructor to create a list of stock and names:
list(portfolio)
Merging is necessary for calculating various stock parameters used in the portfolio optimization models. For this reason, we have a couple of methods.
This is necessary for calculating $\alpha$ and $\beta$ parameters. This is realized by using the function merge_stock_with_benchmark
merged = portfolio.merge_stock_with_benchmark("AAPL")
merged.head()
When merging, it is recommended that you use just those columns which will be required later. Usually the column "Close" is the only one which is useful so it is good idea to use just this column while calling
load_allmethod.
Use the merge_all function for this. This merges all the stocks with benchmark. Note that all stocks must be loaded.
merged_all = portfolio.merge_all()
merged_all.columns
Since we used all the columns while loading, after the merge_all, you get huge number of columns.
Return of a stock is its one of the most important feature. The Portfolio class provides a number of way to get this.
Of course, you can get the return by calling the methods inbuilt in the Stock object. Here, we'll discuss methods of the Portfolio object.
Both of these methods as well as most of the method discussed below takes a parameter
columndictating which column to use while calculating the corresponding values. The default is "Close" and you should not change this. An exception is when you want to use the "Adj. Close". However, in that case too, it is recommended that you change the column name from "Adj. Close" to "Close" while loading the data.
This can be determined using the get_stock_return method. As usualy, pass the name of the stock. The method also taked a frequency parameter.
apple_return, apple_std = portfolio.get_stock_return("AAPL")
apple_return, apple_std
The methods in this object are implemented to give an average return. If you want to get a series of return, use the methods of the
Stockobject.
Use the get_all_stock_returns function!
monthly_returns = portfolio.get_all_stock_returns()
monthly_returns
Well, you can use the portfolio_return method to get this. The function gives a weighted return. You can also specify the weights.
portfolio_return_equal, _ = portfolio.portfolio_return()
portfolio_return_equal
portfolio_return_just_apple, _ = portfolio.portfolio_return(weights=[1,0,0])
portfolio_return_just_apple
These two parameters are required for the CAPM and SIM models. There are two methods for calculating this:
get_stock_params¶This function returns the parameters for one stock identified by the name of the stock.
tesla_alpha, tesla_beta = portfolio.get_stock_params("TSLA")
print(tesla_alpha, tesla_beta)
get_all_stock_params¶This returns parameters for all the stocks in the portfolio.
alpha_beta_all = portfolio.get_all_stock_params(return_dict=False, column="Close")
alpha_beta_all
After using get_all_stock_params method, the alpha and beta of a stock can also be accessed thorugh the attribute of that stock.
for stock in portfolio.stocks:
print(stock.name, stock.alpha, stock.beta)
The parameters can also be accessed directly from the Portfolio:
portfolio.alphas, portfolio.betas
Portfolio object has a summary method which gives summary of the portfolio. The method requires frequency, weights and column:
portfolio.summary()
If you are feeling lazy and don't want to call a couple of methods to calculate the
return,alphaandbeta, you can just vcall thesummarymethod and it calculates all the values under the hood!
The calculation of FFF parameters, however, is not included in the
summarymethod. The reason is that calculations of FFF parameters are a bit involved and unless you want to optimize portfolio using thefff3orfff5model, you don't even need to do the calculations of FFF parameters.
To use the Fama–French three-factor model or five factor model, you need the three or five parameters. As usual, we have two methods to do this:
calculate_fff_params_one¶This calculates the FFF params for the given stock. You can pass the name of the stock or the stock itself. The function uses the Stock.load_fff method to load the FFF data. See the corresponding section for more detail.
apple_fff5 = portfolio.calculate_fff_params_one("AAPL", frequency="M", factors=5, directory="Data")
apple_fff5
calculate_fff_params¶You already know what this method does!
all_ff5 = portfolio.calculate_fff_params(frequency="M", factors=5, directory="Data", verbose=0)
One you have calculated the fff parameters, you can access this with the params attribute of Stock object.
portfolio["AAPL"].params
Or use the stock_params attribute of the Portfolio object:
portfolio.stock_params
These values are required while calculating the expected stock return using fff3 or fff5 method. If you have called calculate_fff_params_one or calculate_fff_params method, yoy don't need to do anything else. The mean values have been calculated and can be accessed by mean_values attribute. If you have not called at least one of these methods, well, call it!
portfolio.mean_values
Model Class¶This class has methods to optimize the portfolio. The class is build on top of the Portfolio class. Let's get started!
Model¶Let's instantiate the model:
model = Model("M")
The only parameters which the Model expects are the frequency and risk_free_rate.
The easiest way to get started with Model is by using the create_portfolio method. This method creates a portfolio by using the benchmark_dir, benchmark_name, stock_dirs, and stock_names. The method accepts some other parameters which are necessary to create a Portfolio.
benchmark_dir = "Data/GSPC.csv"
benchmark_name = "S&P"
stock_dirs = ["Data/AAPL.csv", "Data/MSFT.csv", "Data/GOOG.csv", "Data/TSLA.csv"]
stock_names = ["AAPL", "MSFT", "GOOG", "TSLA"]
frequency = "M"
start_date = "2012-01-01"
end_date = "2022-12-20"
portfolio = model.create_portfolio(
benchmark_dir=benchmark_dir,
benchmark_name=benchmark_name,
stock_dirs=stock_dirs,
stock_names=stock_names,
frequency=frequency,
start_date=start_date,
end_date=end_date
)
The create_portfolio method returns the Portfolio object. It does all the work of loading the data, merging the data and calculating the parameters. If your goal is to optimize portfolio using capm or sim model, you don't need to do anything else. Just call the optimize_portfolio method.
This method, by default, loads just the "Adj. Close" column and renames it to "Close" column.
Though this method is enough for many works, it is not recommended way to use the module. You should create a Portfolio object and then use other method to add it to the Model object.
Start by creating a Portfolio object. Then, use the add_portfolio method to add it to the Model object.
benchmark_dir = "Data/GSPC.csv"
benchmark_name = "S&P"
stock_dirs = ["Data/AAPL.csv", "Data/MSFT.csv", "Data/GOOG.csv", "Data/TSLA.csv"]
stock_names = ["AAPL", "MSFT", "GOOG", "TSLA"]
frequency = "M"
pt = Portfolio(benchmark_dir, benchmark_name, stock_dirs, stock_names)
start_date = "2012-01-01"
end_date = "2022-12-20"
pt.load_benchmark(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
pt.load_all(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
Let's print the portfolio summary:
pt.summary()
Note that you need to calculate the FFF parameters explicitly if you want to use the FFF models. Let's do that:
pt.calculate_fff_params(frequency="M", factors=5, directory="Data", verbose=0)
Great! Now you can optimize the portfolio. But there is another method which we need to discuss.
The Model object accepts just one Portfolio. You can update the portfolio with another one:
benchmark_dir = "Data/GSPC.csv"
benchmark_name = "S&P"
stock_dirs = ["Data/AAPL.csv", "Data/MSFT.csv", "Data/GOOG.csv"]
stock_names = ["AAPL", "MSFT", "GOOG"]
frequency = "M"
pt2 = Portfolio(benchmark_dir, benchmark_name, stock_dirs, stock_names)
start_date = "2012-01-01"
end_date = "2022-12-20"
pt2.load_benchmark(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
pt2.load_all(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
model.portfolio
model.update_portfolio(pt2, weights="equal")
model.portfolio
The function calls the Portfolio.summary() method to make the model ready for optimization.
load_portfolio Function¶Suppose yoy created a Portfolio but have not loaded the data yet. You then add this to the Model by setting the portfolio attribute. You can use Model.portfolio attribute to load the data of benchmark and stocks, or, you can use the load_portfolio method which does all this.
benchmark_dir = "Data/GSPC.csv"
benchmark_name = "S&P"
stock_dirs = ["Data/AAPL.csv", "Data/MSFT.csv", "Data/GOOG.csv"]
stock_names = ["AAPL", "MSFT", "GOOG"]
frequency = "M"
pt2 = Portfolio(benchmark_dir, benchmark_name, stock_dirs, stock_names)
start_date = "2012-01-01"
end_date = "2022-12-20"
model = Model("M")
model.portfolio = pt2
model.portfolio
model.load_portfolio(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
Before optimizing the portfolio, suppose you want to try some weights and see how the return and risk is changing. Or you just want to see expected return of a stock based on its calculated parameters. For this the Model has some methods. Let's create a model:
benchmark_dir = "Data/GSPC.csv"
benchmark_name = "S&P"
stock_dirs = ["Data/AAPL.csv", "Data/MSFT.csv", "Data/GOOG.csv", "Data/TSLA.csv"]
stock_names = ["AAPL", "MSFT", "GOOG", "TSLA"]
frequency = "M"
pt = Portfolio(benchmark_dir, benchmark_name, stock_dirs, stock_names)
start_date = "2012-01-01"
end_date = "2022-12-20"
pt.load_benchmark(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
pt.load_all(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
model = Model()
model.add_portfolio(pt, weights="equal")
We won't calculate the FFF parameters just yet.
expected_return_of_stock¶This function returns what the name says:
exp_return = model.expected_return_of_stock(pt["AAPL"], model="capm")
exp_return
exp_return = model.expected_return_of_stock(pt["AAPL"], model="sim")
exp_return
Returns by the capm and sim models are almost same. Let's try FFF models. As the warning message says, we have to first do the FFF calculations.
pt.calculate_fff_params(frequency="M", factors=5, directory="Data", verbose=0)
exp_return = model.expected_return_of_stock(pt["AAPL"], model="fff5")
exp_return
exp_return = model.expected_return_of_stock(pt["AAPL"], model="fff3")
exp_return
So, the FFF models predict higher returns!
portfolio_info¶This method returns the expected value and risk of portfolio given the weights and model:
model.portfolio
weights = "equal"
model_ = "capm"
exp_return, variance, _ = model.portfolio_info(weights=weights, model=model_)
print(f"Expected Return: {exp_return:.2f}%")
print(f"Expected Variance: {variance:.2f}")
weights = "equal"
model_ = "fff5"
exp_return, variance, _ = model.portfolio_info(weights=weights, model=model_)
print(f"Expected Return: {exp_return:.2f}%")
print(f"Expected Variance: {variance:.2f}")
weights = [0.2, 0.2, 0.2, 0.4]
model_ = "fff5"
exp_return, variance, _ = model.portfolio_info(weights=weights, model=model_)
print(f"Expected Return: {exp_return:.2f}%")
print(f"Expected Variance: {variance:.2f}")
If you have just two stocks in your portfolio, you can use the portfolio_frontier method to plot the portfolio frontier with a model.
pt.remove_stocks(["TSLA", "MSFT"])
model.portfolio
As you have deleted two stocks, you need to call summary again to recalculate the params.
pt.summary()
You will also need to delete the series of calculted FFF params for these stocks.
del pt.stock_params["MSFT"]
del pt.stock_params["TSLA"]
model.portfolio.stock_params
model.portfolio_frontier(model="capm")
model.portfolio_frontier(model="sim")
model.portfolio_frontier(model="fff3")
model.portfolio_frontier(model="fff5")
The fff3 is coming out to be very different.
Okay, let's optimize the following Portfolio:
benchmark_dir = "Data/GSPC.csv"
benchmark_name = "S&P"
stock_dirs = ["Data/AAPL.csv", "Data/MSFT.csv", "Data/GOOG.csv", "Data/TSLA.csv"]
stock_names = ["AAPL", "MSFT", "GOOG", "TSLA"]
frequency = "M"
pt = Portfolio(benchmark_dir, benchmark_name, stock_dirs, stock_names)
start_date = "2012-01-01"
end_date = "2022-12-20"
pt.load_benchmark(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
pt.load_all(
columns=["Adj Close"],
rename_cols=["Close"],
start_date=start_date,
end_date=end_date,
frequency=frequency,
)
model = Model()
model.add_portfolio(pt, weights="equal")
pt.calculate_fff_params(frequency="M", factors=5, directory="Data", verbose=0)
All set! All you need now is to call optimize_portfolio with model, risk and can_short parameter. You may call the portfolio_info first with default parameters. This will give you an idea about how much risk to consider.
model.portfolio_info()
It seems that the variance of the Portfolio with "equal" weights is 0.551. Let's see what is the maximum return at that risk.
def get_return(risk, can_short):
models = ["capm", "sim", "fff3", "fff5"]
for m in models:
print(f"Optimizing for -> {m.upper()}")
_ = model.optimize_portfolio(m, risk=risk, can_short=can_short)
print()
risk = 0.5
can_short = False
get_return(risk, can_short)
So, FFF3 model gives the best return of 2.336% for the weights:
AAPL: 0.00%
MSFT: 53.08%
GOOG: 23.86%
TSLA: 23.06%
Let's allow shorting:
risk = 0.5
can_short = True
get_return(risk, can_short)
Very little increase in maximum return in observed (2.339%) for
AAPL: -5.51%
MSFT: 56.67%
GOOG: 25.98%
TSLA: 22.86%
Let's increase the risk to 1:
risk = 1
can_short = False
get_return(risk, can_short)
Maximum return is increased (at it should be). The new maximum return is 3.0121% for
AAPL: 0.00%
MSFT: 47.74%
GOOG: 6.33%
TSLA: 45.92%
risk = 1
can_short = True
get_return(risk, can_short)
Allowing for short does not have very large effect.
At last, we'll consider a very small risk.
risk = 0.1
can_short = False
get_return(risk, can_short)
The model can not optimize for this low risk. The best result is is:
Expected return: 1.6268%
Variance: 0.2992%
Expected weights:
AAPL: 14.90%
MSFT: 47.07%
GOOG: 38.03%
TSLA: 0.00%